package cn.yangliu.controller;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Pair;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.RuntimeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.system.oshi.OshiUtil;
import cn.yangliu.annotation.GlobalResponse;
import cn.yangliu.config.AutoResponseCommandContainer;
import cn.yangliu.config.SerialPortContainer;
import cn.yangliu.config.XlsxChangeEvent;
import cn.yangliu.constant.CallbackFunction;
import cn.yangliu.constant.ResultCode;
import cn.yangliu.entity.Dictionary;
import cn.yangliu.entity.OftenUseCommand;
import cn.yangliu.entity.Setting;
import cn.yangliu.entity.SettingDetail;
import cn.yangliu.entity.ToolApplicationEvent;
import cn.yangliu.ex.NoSuchPortException;
import cn.yangliu.ex.PortOpenException;
import cn.yangliu.ex.ToolException;
import cn.yangliu.model.CallbackParams;
import cn.yangliu.model.CmdInfo;
import cn.yangliu.model.ListenInfo;
import cn.yangliu.model.OftenUseCommandIndfo;
import cn.yangliu.model.Result;
import cn.yangliu.monitor.XlsxAlterationMonitor;
import cn.yangliu.service.CacheService;
import cn.yangliu.service.DictionaryService;
import cn.yangliu.service.OftenUseCommandService;
import cn.yangliu.service.SettingDetailService;
import cn.yangliu.service.SettingService;
import cn.yangliu.tools.AppHandler;
import cn.yangliu.tools.ExcelReadHandler;
import cn.yangliu.tools.SerialPortManager;
import cn.yangliu.tools.UpgradeHandler;
import com.fazecast.jSerialComm.SerialPort;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.event.EventListener;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import oshi.software.os.OperatingSystem;

import javax.annotation.PreDestroy;
import java.io.File;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import static cn.yangliu.constant.Constants.EXAMPLE;
import static cn.yangliu.constant.Constants.FILE_WATCH_INTERVAL;
import static cn.yangliu.constant.Constants.USER_HOME;
import static cn.yangliu.tools.AppHandler.SERIAL_PORT_DRIVER;
import static cn.yangliu.tools.AppHandler.USER_DIR;

/**
 * the type of com-tool
 *
 * <p>
 * .
 *
 * @author 杨柳
 * @date 2021-09-28
 */
@Slf4j
@RestController
@GlobalResponse
public class MainController implements ApplicationContextAware {

    /**
     * xlsx监视计数
     */
    private static final Map<String, Integer> COUNT_MAP = new HashMap<>();

    /**
     * inject settingService
     */
    @Autowired
    private SettingService settingService;

    /**
     * inject dictionaryService
     */
    @Autowired
    private DictionaryService dictionaryService;

    /**
     * inject settingDetailService
     */
    @Autowired
    private SettingDetailService settingDetailService;

    /**
     * inject serialPortContainer
     */
    @Autowired
    private SerialPortContainer serialPortContainer;

    /**
     * inject excelReadHandler
     */
    @Autowired
    private ExcelReadHandler excelReadHandler;

    /**
     * inject fileAlterationListener
     */
    @Autowired
    private FileAlterationListenerAdaptor fileAlterationListener;

    /**
     * inject autoResponseCommandContainer
     */
    @Autowired
    private AutoResponseCommandContainer autoResponseCommandContainer;

    /**
     * inject cacheService
     */
    @Autowired
    private CacheService cacheService;

    /**
     * inject upgradeHandler
     */
    @Autowired
    private UpgradeHandler upgradeHandler;

    /**
     * inject oftenUseCommandService
     */
    @Autowired
    private OftenUseCommandService oftenUseCommandService;
    /**
     * 添加前后缀的setting
     * Map<配置名, 配置详细>
     */
    private static final Map<String, SettingDetail> ADDTION_MAP = new HashMap<>();

    /**
     * Map<监听文件绝对路径, 文件变化监视器>
     */
    private final Map<String, XlsxAlterationMonitor> listenFiles = new HashMap<>();

    /**
     * 获取所有配置
     * @return 所有配置JSON
     */
    @GetMapping("/getAllSettings")
    public List<Setting> getAllSettings() {
        return settingService.findAll();
    }

    /**
     * 获取所有配置
     * @return 所有配置JSON
     */
    @GetMapping("/getSetting/{settingId}")
    public Setting getSetting(@PathVariable("settingId") Long settingId) {
        return settingService.findById(settingId).orElseGet(Setting::new);
    }




    /**
     * 查询数据字典
     *
     * @param groupName 分组名称
     * @return 数据字典json
     */
    @GetMapping("/getDictionaries")
    public List<Dictionary>  getDictionaries(String groupName) {
        return dictionaryService.findByGroupName(groupName);
    }

    /**
     * 查询系统
     *
     * @return 数据字典json
     */
    @GetMapping("/getOS")
    public Dictionary getOS() {
        Dictionary osInfo = new Dictionary();
        osInfo.setName("os");
        OperatingSystem os = OshiUtil.getOs();
        osInfo.setVal(os.getFamily());
        return osInfo;
    }

    /**
     * 保存配置详细
     *
     * @param settingDetail 配置信息
     * @return 带id的数据
     */
    @PostMapping("/saveSettingDetail")
    public SettingDetail saveSettingDetail(@RequestBody SettingDetail settingDetail) {
        settingDetailService.save(settingDetail);
        return settingDetail;
    }

    /**
     * 保存基本配置
     *
     * @param data 基本配置信息json
     * @return 带id的配置
     */
    @PostMapping("/saveSetting")
    public Setting saveSetting(@RequestBody Setting setting) {
        if (Objects.isNull(setting.getId()) && settingService.exist(setting.getName())) {
            throw new ToolException(ResultCode.ERROR_TEMPLATE, "设置名" + setting.getName() + "已存在");
        }
        if (settingService.existNotEqualsId(setting.getName(), setting.getId()).isPresent()) {
            throw new ToolException(ResultCode.ERROR_TEMPLATE, "设置名" + setting.getName() + "已存在");
        }
        settingService.save(setting);
        return setting;
    }

    /**
     * 获取配置详细
     *
     * @param settingId 配置id
     * @return 详细配置信息
     */
    @GetMapping("/getSettingDetail/{settingId}")
    public SettingDetail getSettingDetail(@PathVariable("settingId") Long settingId) {
        return settingDetailService.findBySettingId(settingId);
    }

    @GetMapping("/exit")
    public void exit() {
        AppHandler.exit();
    }

    /**
     * 链接串口
     *
     * @param setting 连接信息
     * @return 是否成功
     */
    @PostMapping("/connect")
    public void connect(@RequestBody Setting setting) {
        try {
            SerialPort serialPort = SerialPortManager.openPort(setting.getSerialPort(), setting.getBaudRate(),
                    setting.getDataBits(), setting.getStopBits(), setting.getParity());
            serialPortContainer.put(setting.getName(), serialPort);
            // 添加串口监听
            SerialPortManager.addListener(serialPort, new SerialPortManager.StatusListener() {
                @Override
                public void dataAvailable() {
                    byte[] data = SerialPortManager.readFromPort(serialPort);
                    String cmd = new String(data);
                    cmd = cmd.substring(1, cmd.length() - 1);
                    log.info("收到数据: {}", cmd);
                    SettingDetail settingDetail = ADDTION_MAP.get(setting.getName());
                    //精确匹配
                    if (Objects.equals(settingDetail.getMatchType(), "exact")) {
                        if (autoResponseCommandContainer.containsCommand(setting.getName(), cmd)) {
                            String sendData = autoResponseCommandContainer.getCommand(setting.getName(), cmd);
                            sendData(settingDetail, sendData);
                        }
                    } else {
                        //模糊匹配
                        String sendData = autoResponseCommandContainer.match(setting.getName(), cmd);
                        if (StrUtil.isNotBlank(sendData)) {
                            sendData(settingDetail, sendData);
                        }
                    }


                    Map<String, String> resultDataMap = new HashMap<>();

                    resultDataMap.put("settingName", setting.getName());
                    resultDataMap.put("string", cmd);
                    resultDataMap.put("hex", HexUtil.encodeHexStr(cmd));

                    callback(new CallbackParams<>(resultDataMap,
                            CallbackFunction.CALLBACK));
                }

                private void sendData(SettingDetail settingDetail, String sendData) {
                    //是否添加前缀和后缀
                    if (CharSequenceUtil.isNotEmpty(settingDetail.getPrefix()) && CharSequenceUtil.isNotEmpty(
                            settingDetail.getSuffix())) {
                        //   添加前缀后缀
                        ADDTION_MAP.put(setting.getName(), settingDetail);
                        Pair<byte[], byte[]> result = parse(settingDetail);

                        ADDTION_MAP.put(setting.getName(), settingDetail);

                        // 添加前后缀
                        sentData(sendData, serialPort, result.getKey(), result.getValue());
                    } else {
                        //不添加前后缀
                        sentData(sendData, serialPort);
                    }

                    Map<String, String> sendDataMap = new HashMap<>();

                    sendDataMap.put("settingName", setting.getName());
                    sendDataMap.put("string", sendData);
                    sendDataMap.put("hex", HexUtil.encodeHexStr(sendData));
                    //将发送的数据回调给前端

                    callback(new CallbackParams<>(sendDataMap,
                            CallbackFunction.CALLBACK_SEND_DATA));
                }

                @Override
                public void clearToSend(SerialPort serialPort) {
                    callback(new CallbackParams<>(new Event(setting.getName(), "CTS", serialPort.getCTS()),
                            CallbackFunction.CALLBACK_STATUS));
                }

                @Override
                public void dataSetReady(SerialPort serialPort) {
                    callback(new CallbackParams<>(new Event(setting.getName(), "DSR", serialPort.getDSR()),
                            CallbackFunction.CALLBACK_STATUS));
                }

                @Override
                public void ringIndicator(SerialPort serialPort) {
                    callback(new CallbackParams<>(new Event(setting.getName(), "RI", serialPort.getRI()),
                            CallbackFunction.CALLBACK_STATUS));
                }

                @Override
                public void carrierDetect(SerialPort serialPort) {
                    callback(new CallbackParams<>(new Event(setting.getName(), "DC", serialPort.getDCD()),
                            CallbackFunction.CALLBACK_STATUS));
                }
            });
        } catch (NoSuchPortException e) {
            throw new ToolException(ResultCode.ERROR_TEMPLATE, e, "串口" + setting.getSerialPort() + "不存在");
        } catch (PortOpenException e) {
            throw new ToolException(ResultCode.ERROR_TEMPLATE, e, "串口" + setting.getSerialPort() + "打开失败");
        }

    }

    /**
     * 关闭连接
     *
     * @param settingName 配置信息
     * @return 是否成功
     */
    @DeleteMapping("/closeConnect")
    public void closeConnect(@RequestParam String settingName) {
        SerialPort serialPort = serialPortContainer.get(settingName);
        SerialPortManager.closePort(serialPort);
        serialPortContainer.remove(settingName);
    }

    /**
     * 手动发送指令
     *
     * @param cmdInfo 串口配置信息
     * @return 是否成功
     */
    @PostMapping("/manualSend")
    public Map<String, String> manualSend(@RequestBody CmdInfo cmdInfo) {
        String sendData = cmdInfo.getSendData();
        SerialPort serialPort = serialPortContainer.get(cmdInfo.getSettingName());
        // 判断是否添加前后缀
        if (CharSequenceUtil.isNotEmpty(cmdInfo.getPrefix()) && CharSequenceUtil.isNotEmpty(
                cmdInfo.getSuffix())) {
            Optional<SettingDetail> optional;
            if (Objects.nonNull(cmdInfo.getId())) {
                optional = settingDetailService.findById(cmdInfo.getId());
            } else {
                SettingDetail settingDetail = new SettingDetail();
                BeanUtils.copyProperties(cmdInfo, settingDetail);
                optional = Optional.of(settingDetail);
            }
            if (optional.isPresent()) {
                SettingDetail settingDetail = optional.get();
                Pair<byte[], byte[]> result = parse(settingDetail);

                // 添加前后缀
                ADDTION_MAP.put(cmdInfo.getSettingName(), settingDetail);
                sentData(sendData, serialPort, result.getKey(), result.getValue());
            }
        } else {
            // 不添加前后缀
            sentData(sendData, serialPort);
        }

        Map<String, String> result = new HashMap<>();
        result.put("string", sendData);
        result.put("hex", HexUtil.encodeHexStr(sendData));
        return result;
    }

    /**
     * 将字符串解析为byte数组
     *
     * @param settingDetail 配置详情
     * @return prefix suffix
     */
    private Pair<byte[], byte[]> parse(SettingDetail settingDetail) {
        String prefix = settingDetail.getPrefix();
        String suffix = settingDetail.getSuffix();
        String[] prefixArray = prefix.split(",");
        String[] suffixArray = suffix.split(",");

        List<Byte> prefixBytes  = new ArrayList<>();
        List<Byte> suffixBytes  = new ArrayList<>();

        for (String s: prefixArray) {
            if (CharSequenceUtil.isNotEmpty(s)) {
                prefixBytes.add(Byte.parseByte(s));
            }
        }
        for (String s: suffixArray) {
            if (CharSequenceUtil.isNotEmpty(s)) {
                suffixBytes.add(Byte.parseByte(s));
            }
        }
        byte[] prefixByteArray = new byte[prefixBytes.size()];
        for (int i = 0; i < prefixBytes.size(); i++) {
            prefixByteArray[i] = prefixBytes.get(i);
        }

        byte[] suffixByteArray = new byte[suffixBytes.size()];
        for (int i = 0; i < suffixBytes.size(); i++) {
            suffixByteArray[i] = suffixBytes.get(i);
        }

        return Pair.of(prefixByteArray, suffixByteArray);
    }

    /**
     * 发送指令
     *
     * @param sendData 指令信息
     * @param serialPort 串口信息
     */
    private void sentData(String sendData, SerialPort serialPort) {
        log.info("发送数据: {}", sendData);
        byte[] bytes = sendData.getBytes(StandardCharsets.UTF_8);
        SerialPortManager.sendToPort(serialPort, bytes);
    }

    /**
     * 发送指令
     *
     * @param sendData 指令信息
     * @param serialPort 串口信息
     * @param prefix 前缀
     * @param suffix 后缀
     */
    private void sentData(String sendData, SerialPort serialPort, byte[] prefix, byte[] suffix) {
        log.info("发送数据: {}", sendData);
        byte[] bytes = sendData.getBytes(StandardCharsets.UTF_8);
        byte[] buffer = new byte[bytes.length + prefix.length + suffix.length];


        System.arraycopy(prefix, 0, buffer, 0, prefix.length);

        System.arraycopy(bytes, 0, buffer, prefix.length, bytes.length);
        System.arraycopy(suffix, 0, buffer, prefix.length + bytes.length, suffix.length);


        SerialPortManager.sendToPort(serialPort, buffer);
    }

    /**
     * 字符串转hex16
     *
     * @param data 原文
     * @return hex16字符串
     */
    @PostMapping("/string2Hex16")
    public Result<String> string2Hex16(String data) {
        return new Result<>(ResultCode.SUCCESS, HexUtil.encodeHexStr(data));
    }

    /**
     * 16进制字符串转普通文本
     *
     * @param data 16进制字符串
     * @return 普通文本
     */
    @PostMapping("/hex16Tostring")
    public Result<String> hex16Tostring(String data) {
        return new Result<>(ResultCode.SUCCESS, HexUtil.decodeHexStr(data));
    }

    /**
     * 监听文件, 根据文件的key自动回复val
     *
     * @return 是否成功
     */
    @PostMapping("/startListen")
    public void startListen(@RequestBody ListenInfo info) {
        try {
            // 先判断文件是否存在
            File file = new File(info.getListenFile());
            if (!file.exists()) {
                //文件不存在
                throw new ToolException(ResultCode.ERROR_TEMPLATE, "加载" + info.getListenFile() + "失败,文件不存在");
            }
            // 读取excel文件指令
            Map<String, String> commands = excelReadHandler.read(info.getListenFile(),
                    info.getSheet());
            //指令键值对缓存到内存中
            autoResponseCommandContainer.put(info.getSettingName(), commands);

            // 先判断文件是否已经开启监听内容改变的监视器
            // 如果没有开启则开启, 并把监视器放到内存缓存起来
            // 先判断文件是否已经开启监听内容改变的监视器
            // 如果没有开启则开启, 并把监视器放到内存缓存起来
            if (!listenFiles.containsKey(info.getListenFile())) {
                //查询默认监视间隔时间 默认2秒
                Dictionary cache = dictionaryService.findByKey(FILE_WATCH_INTERVAL);
                XlsxAlterationMonitor monitor;
                // 开启文件监听功能, 监听文件内容是否改变
                monitor = new XlsxAlterationMonitor(file.getParentFile().getAbsolutePath(), file.getName(), cache.intValue() * 1000L, fileAlterationListener);

                // 将监视器放到内存中缓存起来
                listenFiles.put(info.getListenFile(), monitor);
                //开启监视
                monitor.start();
                COUNT_MAP.put(info.getListenFile(), 1);
                COUNT_MAP.put(info.getListenFile(), 1);
            } else {
                //包含
                Integer count = COUNT_MAP.get(info.getListenFile());
                count++;
                COUNT_MAP.put(info.getListenFile(), count);
            }

            SettingDetail settingDetail = new SettingDetail();
            BeanUtils.copyProperties(info, settingDetail);
            ADDTION_MAP.put(info.getSettingName(), settingDetail);
        } catch (Exception e) {
            throw new ToolException(ResultCode.ERROR_TEMPLATE, e, "加载" + info.getListenFile() + "失败");
        }
    }

    /**
     * 关闭监听
     *
     * @param settingName 配置名稱
     * @param listenFile 监听文件路径
     */
    @PostMapping("/stopListen")
    public void stopListen(String settingName, String listenFile) {
        autoResponseCommandContainer.remove(settingName);
        Integer count = COUNT_MAP.get(listenFile);
        if (count > 1) {
            count--;
            COUNT_MAP.put(listenFile, count);
        } else {
            COUNT_MAP.remove(settingName);
            XlsxAlterationMonitor monitor = listenFiles.remove(listenFile);
            if (Objects.nonNull(monitor)){
                monitor.stop();
            }

        }
    }

    /**
     * 删除串口配置
     * @param settingId 配置id
     */
    @DeleteMapping("/deleteSetting/{settingId}")
    public void deleteSetting(@PathVariable("settingId") Long settingId) {
        Optional<Setting> optional = settingService.findById(settingId);
        if (!optional.isPresent()) {
            return;
        }
        settingService.deleteById(settingId);
        settingDetailService.deleteBySettingId(settingId);
    }


    /**
     * 开发虚拟串口工具
     * 该工具为第三方的, 因此放在ext目录
     */
    @PostMapping("/openVirtualSerialPortDriver")
    public void openVirtualSerialPortDriver() {
        try {
            String file = USER_DIR + File.separator + "ext" + File.separator + SERIAL_PORT_DRIVER + File.separator + "vspdconfig.exe";
            if (!new File(file).exists()) {
                throw new ToolException(ResultCode.ERROR_TEMPLATE, "启动虚拟串口工具失败, 文件或目录不存在");
            }
            // 执行cmd命令 打开程序
            String cmd = "cmd /c " + file;
            RuntimeUtil.exec(cmd);
        } catch (Exception e) {
            throw new ToolException(ResultCode.ERROR_TEMPLATE, "启动虚拟串口工具失败");
        }

    }

    /**
     * 加载常用指令
     *
     * @return 常用指令
     */
    @GetMapping("/loadOftenUseCommands/{settingId}")
    public List<OftenUseCommand> loadOftenUseCommands(@PathVariable("settingId") Long settingId){
        return oftenUseCommandService.findBySettingId(settingId);
    }

    /**
     * 保存常用指令
     *
     */
    @PostMapping("/saveOftenUseCommands")
    public void saveOftenUseCommands(@RequestBody OftenUseCommandIndfo info){
        oftenUseCommandService.deleteBySettingId(info.getSettingId());
        List<OftenUseCommand> list = info.getList();
        list.forEach(data-> data.setSettingId(info.getSettingId()));
        oftenUseCommandService.saveAll(list);
    }


    /**
     * 加载数据字典
     *
     * @param key 字典key
     * @return 数据字典
     */
    @GetMapping("/getDictionary")
    public Dictionary loadCache(String key) {
        Optional<Dictionary> optional = dictionaryService.findByName(key);
        return optional.orElseGet(Dictionary::new);
    }


    /**
     * 保存数据字典
     *
     * @param json 数据字典
     */
    @PostMapping("/saveDictionary")
    public void saveDictionary(@RequestBody List<Dictionary> dictionaries) {
        for (Dictionary dictionary: dictionaries) {
            Optional<Dictionary> optional = dictionaryService.findByName(dictionary.getName());
            Dictionary dbData;
            if (optional.isPresent()) {
                dbData = optional.get();
                dbData.setVal(dictionary.getVal());
            } else {
                dbData = dictionary;
            }
            dictionaryService.save(dbData);
        }
        upgradeHandler.hook();
    }

    /**
     * 保存眼里xlsx文件
     */
    @GetMapping("/saveExampleExcel")
    public Result<String> saveExampleExcel() {
        Resource resource = new ClassPathResource(EXAMPLE);
        String path = USER_HOME + File.separator + "example.xlsx";
        try (InputStream is = resource.getInputStream()) {
            FileUtil.writeFromStream(is, path);
        } catch (Exception e) {
            throw new ToolException(ResultCode.ERROR_TEMPLATE, "保存样例文件失败");
        }
        return new Result<>(ResultCode.SUCCESS, path);
    }

    /**
     * 监听文件变化事件
     *
     * @param xlsxChangeEvent 事件
     */
    @EventListener
    public void listenFileChange(XlsxChangeEvent xlsxChangeEvent){
        File source = (File)xlsxChangeEvent.getSource();
        String path = source.getAbsolutePath();
        // windows路径分隔符json序列化有问题 因此需要临时改变
        callback(new CallbackParams<>(path.replace("\\", "___"),
                CallbackFunction.XLSX_CHANGE_CALLBACK));
    }


    /**
     * 查询状态
     */
    @GetMapping("/getStatus")
    public Map<String, Boolean> getStatus(@RequestParam("settingName") String settingName){
        SerialPort serialPort = serialPortContainer.get(settingName);
        boolean cd = serialPort.getDCD();
        boolean cts = serialPort.getCTS();
        boolean dsr = serialPort.getDSR();
        boolean ri = serialPort.getRI();
        Map<String, Boolean> status = new HashMap<>();
        status.put("dc", cd);
        status.put("cts", cts);
        status.put("dsr", dsr);
        status.put("ri", ri);
        return status;
    }

    /**
     * 程序退出时候的收尾工作
     */
    @PreDestroy
    public void destroy(){
        Collection<XlsxAlterationMonitor> values = listenFiles.values();
        for (XlsxAlterationMonitor value : values) {
            value.stop();
        }
    }

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Data
    @AllArgsConstructor
    static class Event {
        private String settingName;
        private String eventType;
        private boolean value;

    }

    private void callback(CallbackParams<?> params){
        applicationContext.publishEvent(new ToolApplicationEvent(params));
    }
}